// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.IO;
using System.Text;
using Newtonsoft.Json;

namespace SnapObjects.Data
{
    internal class JNodeReader : INodeReader
    {
        private readonly JsonReader _reader;
        private readonly FloatParseHandling _floatParseHandling;

        public static INodeReader Parse(string raw)
        {
            return new JNodeReader(raw);
        }

        public static INodeReader Parse(Stream raw)
        {
            return new JNodeReader(raw);
        }

        private JNodeReader(string raw)
        {
            if (String.IsNullOrWhiteSpace(raw))
            {
                throw new JsonReaderException(Properties.Resource.DATAREADER_NOTNULL_EXCEPTION);
            }

            _reader = new JsonTextReader(new StringReader(raw));

            _floatParseHandling = _reader.FloatParseHandling;

            this.InitNode();
        }

        private JNodeReader(Stream raw)
        {
            if (raw == null || raw.Length <= 0)
            {
                throw new JsonReaderException(Properties.Resource.DATAREADER_NOTNULL_EXCEPTION);
            }

            _reader = new JsonTextReader(new StreamReader(raw));

            _floatParseHandling = _reader.FloatParseHandling;

            this.InitNode();
        }

        private JNodeReader(JsonReader reader, string name, DataNodeType type, bool isChildNode)
        {
            _reader = reader;

            this.Name = name;
            this.Depth = reader.Depth;
            this.NodeType = type;
            this.CurrentNodeName = name;
            this.IsChildNode = isChildNode;
        }

        private bool IsChildNode { get; }

        public int Depth { get; private set; }
        public bool IsEnd { get; private set; }
        public bool IsEmpty { get; private set; }
        public string Name { get; private set; }
        public string CurrentNodeName { get; private set; }
        public DataNodeType NodeType { get; private set; }
        public DataNodeType CurrentNodeType { get; private set; }

        public IValueReader ValueReader
        {
            get
            {
                this.CheckEnd();

                if (this.CurrentNodeType != DataNodeType.Value)
                {
                    throw new JsonReaderException($"Unexpected token '{this.Name}'.");
                }

                return new JValueReader(this.CurrentNodeName, _reader.Value);
            }
        }

        public INodeReader ReadAsNode()
        {
            this.CheckEnd();

            if ((this.CurrentNodeType == DataNodeType.Object &&
                _reader.TokenType == JsonToken.StartObject) ||
                (this.CurrentNodeType == DataNodeType.Array &&
                _reader.TokenType == JsonToken.StartArray))
            {
                return new JNodeReader(_reader, this.CurrentNodeName, this.CurrentNodeType, true);
            }
            else
            {
                throw new JsonReaderException($"Unexpected token '{_reader.Value}'.");
            }
        }

        public IValueReader ReadAsRaw()
        {
            this.CheckEnd();

            var name = this.CurrentNodeName;
            var builder = new StringBuilder();

            if (_reader.TokenType == JsonToken.PropertyName)
            {
                name = _reader.Value?.ToString();

                _reader.Read();
            }

            if (_reader.TokenType == JsonToken.StartArray ||
                _reader.TokenType == JsonToken.StartObject)
            {
                var depth = _reader.Depth;

                var writer = new JsonTextWriter(new StringWriter(builder));

                this.WriteToken(writer);

                while (_reader.Read() && (depth < _reader.Depth))
                {
                    this.WriteToken(writer);
                }

                this.WriteToken(writer);
                this.Read();

                return new JValueReader(name, builder.ToString());
            }
            else
            {
                var value = _reader.Value;

                this.Read();

                return new JValueReader(name, value?.ToString());
            }
        }

        public bool Read()
        {
            this.CheckEnd();

            var canRead = _reader.Read();

            if (!canRead ||
                (_reader.TokenType == JsonToken.EndObject && this.NodeType == DataNodeType.Object) ||
                (_reader.TokenType == JsonToken.EndArray && this.NodeType == DataNodeType.Array))
            {
                if (canRead &&
                    !this.IsChildNode &&
                    this.NodeType == DataNodeType.Array &&
                    _reader.TokenType == JsonToken.EndArray)
                {
                    if (!_reader.Read() || _reader.TokenType == JsonToken.EndArray)
                    {
                        this.IsEnd = true;

                        return false;
                    }
                }
                else
                {
                    this.IsEnd = true;

                    return false;
                }
            }

            if (this.NodeType == DataNodeType.Object)
            {
                this.CurrentNodeName = _reader.Value?.ToString();

                if (!_reader.Read())
                {
                    this.IsEnd = true;

                    return false;
                }
            }

            switch (_reader.TokenType)
            {
                case JsonToken.StartObject: this.CurrentNodeType = DataNodeType.Object; break;
                case JsonToken.StartArray: this.CurrentNodeType = DataNodeType.Array; break;
                default: this.CurrentNodeType = DataNodeType.Value; break;
            }

            return true;
        }

        public void SkipCurrentNode()
        {
            this.CheckEnd();

            _reader.Skip();
        }

        private void InitNode()
        {
            if (!_reader.Read())
            {
                this.IsEnd = true;

                return;
            }

            if (_reader.TokenType == JsonToken.StartObject)
            {
                this.NodeType = DataNodeType.Object;
            }
            else if (_reader.TokenType == JsonToken.StartArray)
            {
                this.NodeType = DataNodeType.Array;
            }
            else
            {
                throw new JsonReaderException($"Unexpected token '{_reader.Value}'.");
            }

            this.IsEnd = false;
        }

        private void CheckEnd()
        {
            if (this.IsEnd)
            {
                throw new JsonReaderException("The node has been read to the end.");
            }
        }

        private void WriteToken(JsonWriter writer)
        {
            var text = _reader.Value?.ToString();

            switch (_reader.TokenType)
            {
                case JsonToken.StartObject: writer.WriteStartObject(); break;
                case JsonToken.EndObject: writer.WriteEndObject(); break;
                case JsonToken.StartArray: writer.WriteStartArray(); break;
                case JsonToken.EndArray: writer.WriteEndArray(); break;

                case JsonToken.PropertyName: writer.WritePropertyName(text); break;
                case JsonToken.Comment: writer.WriteComment(text); break;
                case JsonToken.Raw: writer.WriteRaw(text); break;

                case JsonToken.Integer:
                case JsonToken.Float:
                case JsonToken.String:
                case JsonToken.Boolean:
                case JsonToken.Null:
                case JsonToken.Date:
                case JsonToken.Bytes:
                    writer.WriteValue(_reader.Value);
                    break;
            }
        }

        internal void ParseFloatByDecimal()
        {
            _reader.FloatParseHandling = FloatParseHandling.Decimal;
        }

        internal void ParseFloatByDefault()
        {
            _reader.FloatParseHandling = _floatParseHandling;
        }
    }
}
